/*
 * OpenCms Ant Task to create a module build tree from an OpenCms manifest.xml files. Copyright (C) 2009 Achim Westermann:
 * 
 * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */
package com.eurelis.opencms.ant.task;

import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.util.FileUtils;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentFactory;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;

/**
 * 
 * ANT task that will create a a module build tree that works for the OpenCms module build with the <code>{@link ManifestBuilderTask}</code>
 * .
 * <p>
 * 
 * @author Achim Westermann
 * 
 */
public class OpenCmsModuleDeflaterTask extends Task {
    /**
     * Models a dependency of the module.
     * <p>
     * 
     */
    public class Dependency {

        String m_DependencyName;

        String m_dependencyVersion;

        public Dependency() {

            super();
        }

        public String getName() {
            return this.m_DependencyName;
        }

        public String getVersion() {
            return this.m_dependencyVersion;
        }

        public void setName(final String dependencyName) {
            this.m_DependencyName = dependencyName;
        }

        public void setVersion(final String dependencyVersion) {
            this.m_dependencyVersion = dependencyVersion;
        }

        /**
         * Returns an XML representation.
         * <p>
         */
        @Override
        public String toString() {
            final StringBuilder result = new StringBuilder("<dependency name=\"").append(this.m_DependencyName).append("\" version=\"")
                            .append(this.m_dependencyVersion).append("\" />");
            return result.toString();
        }
    }

    /**
     * Models an export point of the module.
     * <p>
     */
    public class ExportPoint {

        String m_exportpointDst;

        String m_exportpointSrc;

        public ExportPoint() {
            super();
        }

        public String getDst() {
            return this.m_exportpointDst;
        }

        public String getSrc() {
            return this.m_exportpointSrc;
        }

        public void setDst(final String dst) {
            this.m_exportpointDst = dst;
        }

        public void setSrc(final String src) {
            this.m_exportpointSrc = src;
        }

        /**
         * Returns an XML representation.
         * <p>
         */

        @Override
        public String toString() {
            final StringBuilder result = new StringBuilder("<exportpoint uri=\"");
            result.append(this.m_exportpointSrc);
            result.append("\" destination=\"");
            result.append(this.m_exportpointDst);
            result.append("\"/>");
            return result.toString();
        }
    }

    /**
     * Models a parameter of the module.
     * <p>
     */
    public class Parameter {

        String m_parameterName;

        String m_parameterValue;

        public Parameter() {
            super();
        }

        public String getName() {
            return this.m_parameterName;
        }

        public String getValue() {
            return this.m_parameterValue;
        }

        public void setName(final String name) {
            this.m_parameterName = name;
        }

        public void setValue(final String value) {
            this.m_parameterValue = value;
        }

        /**
         * Returns an XML representation.
         * <p>
         */
        @Override
        public String toString() {
            final StringBuilder result = new StringBuilder("<parameter name=\"");
            result.append(this.m_parameterName);
            result.append("\" value=\"");
            result.append(this.m_parameterValue);
            result.append("\"/>");
            return result.toString();
        }
    }

    /**
     * Models a resource of the module.
     * <p>
     */
    public class Resource {

        String m_resourceUri;

        public Resource() {
            super();
        }

        public String getUri() {
            return this.m_resourceUri;
        }

        public void setUri(final String uri) {
            this.m_resourceUri = uri;
        }

        /**
         * Returns an XML representation.
         * <p>
         */
        @Override
        public String toString() {
            final StringBuilder result = new StringBuilder("<resource uri=\"");
            result.append(this.m_resourceUri);
            result.append("\"/>");
            return result.toString();

        }
    }

    /**
     * The magic prefix for a file meta info that is not a property (but e.g. the type or the resource uuid).
     **/
    public static final String MAGIC_FILEPROPERTY_PREFIX = "EurelisProperty.";
    
    /**
     * The magic suffix for a file meta info: the type of file.
     **/
    public static final String MAGIC_FILEPROPERTY_SUFFIX_TYPE = "type";

    /**
     * The magic suffix for a file meta info: the resource uuid of file.
     **/
    public static final String MAGIC_FILEPROPERTY_SUFFIX_RESOURCE_UUID = "resourceUUID";

    /**
     * The magic suffix for a file meta info: the resource uuid of file.
     **/
    public static final String MAGIC_FILEPROPERTY_SUFFIX_STRUCTURE_UUID = "structureUUID";

    /** Copied from opencms to be independent of OpenCms in the compile classpath: Stay in sync. **/
    public static DateFormat OPENCMS_MODULE_DATEFORMAT = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);

    /**
     * Separates the last path token of the path String from the leading ones.
     * <p>
     * 
     * This assumes '/' as the path separator. This works with non-existing files / folders but is not generic.
     * <p>
     * 
     * @param path
     *            the absolute file path you want the mere file name of.
     * 
     * @return the <code>{@link Map.Entry}</code> consisting of path information and file name.
     */
    private static Map.Entry<String, String> cutLastToken(final String path) {

        final StringBuffer dir = new StringBuffer();
        String filenonfinal = "";

        final String fileseparator = "/";// System.getProperty("file.separator");
        final StringTokenizer tokenizer = new StringTokenizer(path, fileseparator);
        final int size = tokenizer.countTokens();
        String token;
        switch (size) {
        case 0:
            break;
        case 1:
            token = tokenizer.nextToken();
            if (new File(path).isDirectory()) {
                dir.append(token);
            } else {
                filenonfinal = path;
            }
            break;

        default:
            while (tokenizer.hasMoreElements()) {
                // reuse String filesparator: bad style...
                token = tokenizer.nextToken();
                if (tokenizer.hasMoreTokens()) {
                    dir.append(token);
                    dir.append(fileseparator);
                } else {
                    filenonfinal = token;
                }
            }
        }

        final String file = filenonfinal;

        return new Map.Entry<String, String>() {

            private final String m_key = dir.toString();
            String m_value = file;

            public String getKey() {
                return this.m_key;
            }

            public String getValue() {
                return this.m_value;
            }

            public String setValue(final String value) {
                final String result = this.m_value;
                this.m_value = value;
                return result;
            }

            @Override
            public String toString() {
                return new StringBuilder("[").append(this.m_key).append(", ").append(this.m_value).append(']').toString();
            }
        };
    }

    private String authoremail;

    private String authorname;

    private String creator;

    private String dateinstalled;

    private final List<Dependency> dependencies;

    Document document;

    private String explorertypes;

    private final List<ExportPoint> exportpoints;

    private String exportversion;

    private final List<FileSet> filesets;

    private String group;

    /** The date of the initial creation of the module. */
    private String m_datecreated;

    /** The create date info of the module export. */
    private String m_exportDateCreated;

    /**
     * If true the files of the module will be copied to the build tree will be created.
     */
    private boolean m_importContentFiles;

    /** The folder that contains the deflated module to import. */
    private String m_importmodulefolder;

    /** If true property-files for the build tree will be created. */
    private boolean m_importPropertyFiles;

    /** The infoproject info of the module. */
    private String m_infoproject;

    /** Location of the build tree of the module. */
    private String m_moduleBuildTreeFolder;

    /** The path to the module info xml file to write. */
    private String m_moduleinfo;

    private String m_moduleName;

    /** The path to the resource type xml file to write. */
    private String m_resourcetypes;

    private String moduleclass;

    private String moduledescription;

    private String nicename;

    private String opencmsversion;

    private final List<Parameter> parameters;

    private final List<Resource> resources;

    private String userinstalled;

    private String version;

    public OpenCmsModuleDeflaterTask() {
        this.document = null;
        this.m_resourcetypes = null;
        this.explorertypes = null;

        this.m_importmodulefolder = null;
        this.exportpoints = new LinkedList<ExportPoint>();
        this.resources = new LinkedList<Resource>();
        this.dependencies = new LinkedList<Dependency>();
        this.parameters = new LinkedList<Parameter>();
        this.filesets = new LinkedList<FileSet>();
        this.creator = "";
        this.opencmsversion = "";
        this.m_infoproject = "";
        this.exportversion = "";
        this.m_moduleName = "";
        this.nicename = "";
        this.group = "";
        this.moduleclass = "";
        this.moduledescription = "";
        this.version = "";
        this.authorname = "";
        this.authoremail = "";
        this.userinstalled = "";
        this.dateinstalled = "";
        this.m_importContentFiles = true;
        this.m_importPropertyFiles = true;
    }

    public void addFileset(final FileSet fileset) {
        this.filesets.add(fileset);
    }

    /**
     * Creates the file containing the VFS meta infos for a file such as type, properties or uuids.
     * <p>
     * 
     * @param relativePath
     *            the relative path from the &lt;destination&gt; node of manifest.xml.
     * 
     * @param type
     *            the type of file.
     * 
     * @param isFolder
     *            if true a folder meta file is uses (special naming convention for those).
     * 
     * @param isSibling
     *            if true a sibling is processed.
     * 
     * @param uuidresource
     *            the resource uuid or null if it is a folder.
     * 
     * @param uuidstructure
     *            the structure uuid.
     * 
     * @param propertiesElements
     *            all &lt;property&gt; entries for the file from the manifest.xml.
     */
    private void createMetaFile(final File buildTreeFolder, final String relativePath, final String type, final boolean isFolder,
                    final boolean isSibling, final String uuidresource, final String uuidstructure, final List<Element> propertiesElements) {

        final Map.Entry<String, String> pathAndFile = OpenCmsModuleDeflaterTask.cutLastToken(relativePath);
        final String fileName = pathAndFile.getValue();
        final String path = pathAndFile.getKey();
        this.log("    File name of " + relativePath + " is " + fileName, Project.MSG_DEBUG);
        this.log("    File path of " + relativePath + " is " + path, Project.MSG_DEBUG);

        final StringBuilder pathBuilder = new StringBuilder("/").append(path).append("__properties");

        final File targetFolder = new File(buildTreeFolder, pathBuilder.toString());
        this.potentialCreateFolder(targetFolder);
        File targetFile = null;
        if (isFolder) {
            targetFile = new File(targetFolder, "__" + fileName + ".properties");
        } else {
            targetFile = new File(targetFolder, fileName + ".properties");
        }
        boolean targetFileExists = false;
        this.log("    Target meta file is " + targetFile.getAbsolutePath(), Project.MSG_VERBOSE);
        if (this.m_importPropertyFiles) {

            if (targetFile.exists()) {
                targetFileExists = true;
                this.log("    Overwriting existing meta file " + targetFile.getAbsolutePath(), Project.MSG_WARN);
            }
            this.potentialCreateFile(targetFile);

            String propertyName;
            String line;
            PrintWriter metaFileStream = null;
            try {
                // Important: java properties files are ISO-8859-1 (because OpenCms uses the standard message bundle mechanism with class
                // Properties: http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html
                metaFileStream = new PrintWriter(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(targetFile), Charset
                                .forName("ISO-8859-1"))));
                metaFileStream.println("# ${property_name}.i : individual property");
                metaFileStream.println("# ${property_name}.s : shared property");
                metaFileStream.println();

                if (isSibling) {
                    metaFileStream.println("# THIS IS A SIBLING. The resource ID points to the corresponding content file.");
                    metaFileStream.println();
                    this.log("    Target meta file for Sibling " + targetFile.getAbsolutePath(), Project.MSG_VERBOSE);
                }
                // type:
                propertyName = MAGIC_FILEPROPERTY_PREFIX + MAGIC_FILEPROPERTY_SUFFIX_TYPE;
                line = propertyName + " = " + type;
                this.log("    " + line, Project.MSG_DEBUG);
                metaFileStream.println("# Special: the type of file (no property)");
                metaFileStream.println(line);

                // structure id:
                propertyName = MAGIC_FILEPROPERTY_PREFIX + MAGIC_FILEPROPERTY_SUFFIX_STRUCTURE_UUID;
                line = propertyName + " = " + uuidstructure;
                this.log("    " + line, Project.MSG_DEBUG);
                metaFileStream.println("# Special: the structure ID (optional, only needed if siblings are of concern)");
                metaFileStream.println(line);

                // resource id:
                if (uuidresource != null) {
                    propertyName = MAGIC_FILEPROPERTY_PREFIX + MAGIC_FILEPROPERTY_SUFFIX_RESOURCE_UUID;
                    line = propertyName + " = " + uuidresource;
                    this.log("    " + line, Project.MSG_DEBUG);
                    metaFileStream.println("# Special: the resource ID (optional, only needed if siblings are of concern)");
                    metaFileStream.println(line);
                }

                this.writePropertiesToMetaFile(propertiesElements, metaFileStream);
            } catch (final IOException ioex) {
                throw new BuildException("Cannot create meta file for " + relativePath, ioex);
            } finally {
                // this should flush it !
                log("    Closing meta file " + targetFile, Project.MSG_VERBOSE);
                FileUtils.close(metaFileStream);
            }
        } else {

            if (isSibling) {
                throw new BuildException("importpropertyfiles is set to false but the file " + relativePath + " is a sibling.");
            }
            if (targetFileExists) {
                // read it to find out differences:
                final Properties props = new Properties();
                try {
                    // TODO use this when jdk 1.6 is mature and in use :
                    // Reader utf8Reader = new InputStreamReader(new
                    // FileInputStream(targetFile), Charset.forName("utf-8"));
                    // props.load(utf8Reader);
                    props.load(new FileInputStream(targetFile));
                    // check type
                    final String propType = props.getProperty("type");
                    if ((propType != null) && (!type.equals(propType))) {
                        throw new BuildException("importpropertyfiles is set to false but type in existing meta file "
                                        + targetFile.getAbsolutePath() + " is " + type + " while the imported file " + relativePath
                                        + " defines " + propType);
                    }

                } catch (final IOException e) {
                    this.log("  Failed to load existing meta file of file " + relativePath + " for verification.", Project.MSG_ERR);
                    this.log(e, Project.MSG_ERR);
                }

            }

            if (!"plain".equals(type)) {
                throw new BuildException("importpropertyfiles is set to false but type of the imported file " + relativePath
                                + " is not plain but " + type + ". importpropertyfiles has to be set to true.");
            }

        }
    }

    @Override
    public void execute() throws BuildException {
        super.execute();
        // Start here
        final Document manifestxml = this.readManifestXML();
        this.processManifestXML(manifestxml);
    }

    private void importAccessControlEntries(final Element accesscontrolElement, final String relativePath, final File buildTreeFolder,
                    final boolean isFolder) {
        final Map.Entry<String, String> pathAndFile = OpenCmsModuleDeflaterTask.cutLastToken(relativePath);
        final String fileName = pathAndFile.getValue();
        final String path = pathAndFile.getKey();
        this.log("    File name of " + relativePath + " is " + fileName, Project.MSG_DEBUG);
        this.log("    File path of " + relativePath + " is " + path, Project.MSG_DEBUG);

        final StringBuilder pathBuilder = new StringBuilder("/").append(path).append("__acl");

        final File targetFolder = new File(buildTreeFolder, pathBuilder.toString());
        this.potentialCreateFolder(targetFolder);
        File targetFile = null;
        if (isFolder) {
            targetFile = new File(targetFolder, "__" + fileName + ".xml");
        } else {
            targetFile = new File(targetFolder, fileName + ".xml");
        }
        boolean targetFileExists = false;

        if (this.m_importPropertyFiles) {

            if (targetFile.exists()) {
                targetFileExists = true;
                this.log("    Overwriting existing meta file " + targetFile.getAbsolutePath(), Project.MSG_WARN);
            }
            this.potentialCreateFile(targetFile);

            XMLWriter writer = null;
            try {
                final OutputFormat format = OutputFormat.createPrettyPrint();
                writer = new XMLWriter(new FileOutputStream(targetFile), format);
                writer.write(accesscontrolElement);

            } catch (final IOException ioex) {
                throw new BuildException("Cannot create access control file for " + relativePath, ioex);
            } finally {
                if (writer != null) {
                    try {
                        writer.flush();
                        writer.close();
                    } catch (final IOException e) {
                        throw new BuildException("Problems closing access control file for " + relativePath, e);
                    }
                }
            }
        } else {
            if (targetFileExists) {
                // TODO: compare both access control contents and warn
            } else {
                this.log("There was an access control list for file " + relativePath
                                + " which is dropped because importpropertyfiles was not set to true", Project.MSG_WARN);
            }
        }

    }

    private void importContentFile(final File contentSourceFile, final File contentTargetFile, long vfsDateLastModified) throws IOException {
        this.log("  Importing to " + contentTargetFile.getAbsolutePath(), Project.MSG_VERBOSE);
        this.potentialCreateFile(contentTargetFile);
        // ...filter, overwrite, preserve last modified date
        FileUtils.getFileUtils().copyFile(contentSourceFile, contentTargetFile, null, true, true);
        contentTargetFile.setLastModified(vfsDateLastModified);
    }

    /**
     * Writes the explorer type xml file to the location given by the <code>explorertypes</code> attribute.
     * <p>
     * 
     * @param explorertypesElement
     *            the XML node containing the explorer types.
     */
    private void importExplorertypes(final Element explorertypesElement) {
        if ((this.explorertypes == null) || this.explorertypes.trim().equals("")) {
            throw new BuildException(
                            "Attribute explorertypes must not be empty. It has to denote a file where explorer type settings will be stored (does not have to exist yet).");
        }
        this.log("Writing explorer types to " + this.explorertypes, Project.MSG_INFO);
        final File explorerTypeFile = new File(this.explorertypes);
        this.potentialCreateFile(explorerTypeFile);
        // It's valid to have no explorertype in manifest.xml
        if (explorertypesElement != null) {
            this.writeXmlSnippet(explorertypesElement, explorerTypeFile);
        } else {
            log("Module does not contain <explorertypes>, empty file created.", Project.MSG_INFO);
        }

    }

    /**
     * Writes the module info xml file to the location given by the <code>moduleinfo</code> attribute.
     * <p>
     * 
     * @param rootElement
     *            the XML root (&lt;export&gt;) node containing the resource types.
     */
    private void importModuleInfo(Element rootElement) {
        if ((this.m_moduleinfo == null) || this.m_moduleinfo.trim().equals("")) {
            throw new BuildException(
                            "Attribute resourcetypes must not be empty. It has to denote a file where resource type settings will be stored (does not have to exist yet).");
        }
        this.log("Writing resource types to " + this.m_moduleinfo, Project.MSG_INFO);
        final File moduleInfoFile = new File(this.m_moduleinfo);
        this.potentialCreateFile(moduleInfoFile);
        // Some XML modifications to the XML.
        Document doc = DocumentFactory.getInstance().createDocument();
        Element moduleInfoNode = doc.addElement("moduleinfo");
        // calling detach only would remove this from the original document!
        Element infoElement = rootElement.element("info");

        infoElement = infoElement.createCopy();
        moduleInfoNode.add(infoElement);
        Element moduleNode = rootElement.element("module");
        // calling detach only would remove this from the original document!
        moduleNode = moduleNode.createCopy();

        // wipe out resourcetypes and explorertypes: For better IDE - overview those go to separate files.
        Element resourceTypesElement = moduleNode.element("resourcetypes");
        if (resourceTypesElement != null) {
            resourceTypesElement.detach();
        }
        Element explorerTypesElement = moduleNode.element("explorertypes");
        if (explorerTypesElement != null) {
            explorerTypesElement.detach();
        }
        moduleInfoNode.add(moduleNode);
        this.writeXmlSnippet(moduleInfoNode, moduleInfoFile);
    }

    private void importRelations(final List<Element> relationElements, final File moduleBuildTreeFolder) {
        this.log("  skipping existing <relations>, this has to be implemented! ", Project.MSG_WARN);

    }

    /**
     * Writes the explorer type xml file to the location given by the <code>resourcetypes</code> attribute.
     * <p>
     * 
     * @param resourcetypesElement
     *            the XML node containing the resource types.
     */
    private void importResourcetypes(final Element resourcetypesElement) {
        if ((this.m_resourcetypes == null) || this.m_resourcetypes.trim().equals("")) {
            throw new BuildException(
                            "Attribute resourcetypes must not be empty. It has to denote a file where resource type settings will be stored (does not have to exist yet).");
        }
        this.log("Writing resource types to " + this.m_resourcetypes, Project.MSG_INFO);
        final File resourceTypeFile = new File(this.m_resourcetypes);
        this.potentialCreateFile(resourceTypeFile);
        // It's valid to have no resourctypes in manifest.xml
        if (resourcetypesElement != null) {
            this.writeXmlSnippet(resourcetypesElement, resourceTypeFile);
        } else {
            log("Module does not contain <resourctypes>, empty file created.", Project.MSG_INFO);
        }

    }

    public boolean isImportcontentfiles() {
        return this.m_importContentFiles;
    }

    public boolean isImportpropertyfiles() {
        return this.m_importPropertyFiles;
    }

    /**
     * Tries to create the given file if it does not exist yet.
     * <p>
     * 
     * @param file
     *            the file to potentially create.
     */
    private void potentialCreateFile(final File file) {
        if (!file.exists()) {
            try {
                boolean success = false;
                final File parentFile = new File(file.getParent());
                this.potentialCreateFolder(parentFile);
                success = file.createNewFile();
                if (success) {
                    this.log("Created non existing file " + file.getAbsolutePath(), Project.MSG_VERBOSE);
                } else {
                    throw new BuildException("Cannot create file " + file.getAbsolutePath());
                }

            } catch (final IOException e) {
                throw new BuildException("Creation of non - existing file " + this.explorertypes + " failed. ", e);
            }
        }
    }

    /**
     * Tries to create the given folder if it does not exist yet.
     * <p>
     * 
     * @param folder
     *            the folder to potentially create.
     */
    private void potentialCreateFolder(final File folder) {
        if (!folder.exists()) {
            final boolean created = folder.mkdirs();
            if (created) {
                this.log("Created non existing folder " + folder.getAbsolutePath(), Project.MSG_VERBOSE);
            } else {
                throw new BuildException("Cannot create folder " + folder.getAbsolutePath());
            }

        }
    }

    /**
     * Imports the files to the build tree.
     * <p>
     * 
     * @param fileElements
     *            the file entries from the manifest.xml
     */
    private void processFileEntries(final List<Element> fileElements) {
        this.log("Starting to import files...", Project.MSG_INFO);

        final File moduleBuildTreeFolder = new File(this.m_moduleBuildTreeFolder);
        this.potentialCreateFolder(moduleBuildTreeFolder);
        this.log("Module folder in build tree is " + moduleBuildTreeFolder.getAbsolutePath(), Project.MSG_INFO);

        final File importModuleFolder = new File(this.m_importmodulefolder);
        // if this does not exist build would have failed earlier
        // (readManifest):
        this.log("Module import folder is " + importModuleFolder.getAbsolutePath(), Project.MSG_INFO);

        for (final Element fileElement : fileElements) {
            this.processFileEntry(fileElement, importModuleFolder, moduleBuildTreeFolder);

        }

    }

    /**
     * Imports the single given file to the build tree.
     * <p>
     * 
     * @param fileElement
     *            the XML part of the manifest.xml representing this file.
     * 
     * @param importModuleFolder
     *            the folder containing the deflated OpenCms module (for content - import).
     * 
     * @param moduleBuildTreeFolder
     *            the target folder.
     */
    @SuppressWarnings("unchecked")
    private void processFileEntry(final Element fileElement, final File importModuleFolder, final File moduleBuildTreeFolder) {

        final Element destinationElement = fileElement.element("destination");
        final String destination = destinationElement.getText();
        // Optional: folders and n-1 siblings do not have the source element:
        final Element sourceElement = fileElement.element("source");
        final String source = (sourceElement != null) ? sourceElement.getText() : null;
        final Element typeElement = fileElement.element("type");
        final String type = typeElement.getText();
        // Optional: OpenCms would always generate this but could also import
        // with missing node:
        final Element uuidstructureElement = fileElement.element("uuidstructure");
        final String uuidstructure = (uuidstructureElement != null) ? uuidstructureElement.getText() : null;
        // Optional: folders do not have it and also OpenCms would invent one if
        // it was missing in an import:
        final Element uuidresourceElement = fileElement.element("uuidresource");
        final String uuidresource = (uuidresourceElement != null) ? uuidresourceElement.getText() : null;
        final Element datelastmodifiedElement = fileElement.element("datelastmodified");
        // This may fail!
        Date datelastmodified = new Date();
        try {
            datelastmodified = OPENCMS_MODULE_DATEFORMAT.parse(datelastmodifiedElement.getText());
        } catch (Throwable nfe) {
            throw new BuildException("Found an illegal date last modified '" + datelastmodifiedElement.getText() + "' for file "
                            + destination + " in manifest.xml.", nfe);
        }
        final Element userlastmodifiedElement = fileElement.element("userlastmodified");
        final String userlastmodified = userlastmodifiedElement.getText();
        final Element datecreatedElement = fileElement.element("datecreated");
        final String datecreated = datecreatedElement.getText();
        final Element usercreatedElement = fileElement.element("usercreated");
        final String usercreated = usercreatedElement.getText();
        final Element flagsElement = fileElement.element("flags");
        final String flags = flagsElement.getText();
        this.log("Processing " + destination, Project.MSG_INFO);
        this.log("  source:" + ((source == null) ? "missing" : source), Project.MSG_DEBUG);
        this.log("  type: " + type, Project.MSG_VERBOSE);
        this.log("  uuidstructure: " + ((uuidstructure == null) ? "missing" : uuidstructure), Project.MSG_DEBUG);
        this.log("  uuidresource: " + ((uuidresource == null) ? "missing" : uuidresource), Project.MSG_DEBUG);
        this.log("  datelastmodified: " + datelastmodified, Project.MSG_DEBUG);
        this.log("  userlastmodified: " + userlastmodified, Project.MSG_DEBUG);
        this.log("  datecreated: " + datecreated, Project.MSG_DEBUG);
        this.log("  usercreated: " + usercreated, Project.MSG_DEBUG);
        this.log("  flags: " + flags, Project.MSG_DEBUG);

        final boolean isFolder = "folder".equals(type);
        if (source != null) {
            this.log("  <source> entry found, this is a file and no sibling too: content will be processed", Project.MSG_DEBUG);
            if (this.m_importContentFiles) {
                final File contentSourceFile = new File(importModuleFolder, source);
                if (!contentSourceFile.exists()) {
                    throw new BuildException("Missing content file " + contentSourceFile.getAbsolutePath() + " for import file "
                                    + destination + " with source - entry " + source);
                }

                this.log("  found content file " + contentSourceFile.getAbsolutePath());
                final File contentTargetFile = new File(moduleBuildTreeFolder, "/" + source);
                try {
                    this.importContentFile(contentSourceFile, contentTargetFile, datelastmodified.getTime());
                } catch (final IOException e) {
                    throw new BuildException("Cannot import content of file entry " + source, e);
                }
            } else {
                this.log("  skipping import of " + source + " as importcontentfiles is deactivated", Project.MSG_INFO);
            }

        }
        if (isFolder) {
            this.log("    Folder detected", Project.MSG_VERBOSE);
        }
        boolean isSibling = false;
        if (!isFolder && (source == null)) {
            this.log("  Sibling detected", Project.MSG_VERBOSE);
            isSibling = true;
        }

        final List<Element> propertiesElements = fileElement.element("properties").elements();
        this.createMetaFile(moduleBuildTreeFolder, destination, type, isFolder, isSibling, uuidresource, uuidstructure, propertiesElements);

        final Element accesscontrolElement = fileElement.element("accesscontrol");
        this.importAccessControlEntries(accesscontrolElement, destination, moduleBuildTreeFolder, isFolder);

        final List<Element> relationElements = fileElement.element("relations").elements();
        this.importRelations(relationElements, moduleBuildTreeFolder);

    }

    /**
     * Collects global data from the module and then iterates over all file entries of the manifest.xml and multiplexes actions.
     * <p>
     * 
     * The XML structure should be controlled completely in this method while helper methods only receive sub nodes at leaf - level.
     * <p>
     * 
     * @param manifestxml
     *            the configuration file of the module.
     */
    @SuppressWarnings("unchecked")
    private void processManifestXML(final Document manifestxml) {
        final Element rootElement = manifestxml.getRootElement();
        this.log("Reading module information...", Project.MSG_INFO);
        this.importModuleInfo(rootElement);

        // Just for debugging START

        // info node:
        this.log("Processing \"<info>\" node.", Project.MSG_DEBUG);
        final Element infoElement = rootElement.element("info");
        final Element creatorElement = infoElement.element("creator");
        this.creator = creatorElement.getText();
        this.log("\"<creator>\" is \"" + this.creator + "\".", Project.MSG_DEBUG);
        final Element opencmsversionElement = infoElement.element("opencms_version");
        this.opencmsversion = opencmsversionElement.getText();
        this.log("\"<opencms_version>\" is \"" + this.opencmsversion + "\".", Project.MSG_DEBUG);
        final Element createdateElement = infoElement.element("createdate");
        this.m_exportDateCreated = createdateElement.getText();
        this.log("\"<createdate>\" is \"" + this.m_exportDateCreated + "\".", Project.MSG_DEBUG);
        final Element infoprojectElement = infoElement.element("infoproject");
        this.m_infoproject = infoprojectElement.getText();
        this.log("\"<infoproject>\" is \"" + this.m_infoproject + "\".", Project.MSG_DEBUG);
        final Element exportversionElement = infoElement.element("export_version");
        this.exportversion = exportversionElement.getText();
        this.log("\"<export_version>\" is \"" + this.exportversion + "\".", Project.MSG_DEBUG);

        // module node:
        this.log("Processing \"<module>\" node.", Project.MSG_DEBUG);
        final Element moduleElement = rootElement.element("module");
        final Element nameElement = moduleElement.element("name");
        this.m_moduleName = nameElement.getText();
        this.log("\"<name>\" is \"" + this.m_moduleName + "\".", Project.MSG_DEBUG);
        final Element nicenameElement = moduleElement.element("nicename");
        this.nicename = nicenameElement.getText();
        this.log("\"<nicename>\" is \"" + this.nicename + "\".", Project.MSG_DEBUG);
        final Element groupElement = moduleElement.element("group");
        this.group = groupElement.getText();
        this.log("\"<group>\" is \"" + this.group + "\".", Project.MSG_DEBUG);
        final Element classElement = moduleElement.element("class");
        this.moduleclass = classElement.getText();
        this.log("\"<moduleclass>\" is \"" + this.moduleclass + "\".", Project.MSG_DEBUG);
        final Element descriptionElement = moduleElement.element("description");
        this.moduledescription = descriptionElement.getText();
        this.log("\"<description>\" is \"" + this.moduledescription + "\".", Project.MSG_DEBUG);
        final Element versionElement = moduleElement.element("version");
        this.version = versionElement.getText();
        this.log("\"<version>\" is \"" + this.version + "\".", Project.MSG_DEBUG);
        final Element authornameElement = moduleElement.element("authorname");
        this.authorname = authornameElement.getText();
        this.log("\"<authorname>\" is \"" + this.authorname + "\".", Project.MSG_DEBUG);
        final Element authoremailElement = moduleElement.element("authoremail");
        this.authoremail = authoremailElement.getText();
        this.log("\"<authoremail>\" is \"" + this.authoremail + "\".", Project.MSG_DEBUG);
        final Element datecreatedElement = moduleElement.element("datecreated");
        this.m_datecreated = datecreatedElement.getText();
        this.log("\"<datecreated>\" is \"" + this.m_datecreated + "\".", Project.MSG_DEBUG);
        final Element userinstalledElement = moduleElement.element("userinstalled");
        this.userinstalled = userinstalledElement.getText();
        this.log("\"<userinstalled>\" is \"" + this.userinstalled + "\".", Project.MSG_DEBUG);
        final Element dateinstalledElement = moduleElement.element("dateinstalled");
        this.dateinstalled = dateinstalledElement.getText();
        this.log("\"<dateinstalled>\" is \"" + this.dateinstalled + "\".", Project.MSG_DEBUG);
        final List<Element> dependencyElements = moduleElement.element("dependencies").elements();
        for (final Element dependencyElement : dependencyElements) {
            final Dependency dependency = new Dependency();
            dependency.setName(dependencyElement.attributeValue("name"));
            dependency.setVersion(dependencyElement.attributeValue("version"));
            this.dependencies.add(dependency);
            this.log("Added dependency " + dependency, Project.MSG_INFO);
        }
        final List<Element> exportpointElements = moduleElement.element("exportpoints").elements();
        for (final Element exportpointElement : exportpointElements) {
            final ExportPoint exportpoint = new ExportPoint();
            exportpoint.setDst(exportpointElement.attributeValue("destination"));
            exportpoint.setSrc(exportpointElement.attributeValue("uri"));
            this.exportpoints.add(exportpoint);
            this.log("Added exportpoint " + exportpoint, Project.MSG_INFO);
        }
        final List<Element> resourceElements = moduleElement.element("resources").elements();
        for (final Element resourceElement : resourceElements) {
            final Resource resource = new Resource();
            resource.setUri(resourceElement.attributeValue("uri"));
            this.resources.add(resource);
            this.log("Added module resource " + resource, Project.MSG_INFO);
        }
        final List<Element> paramElements = moduleElement.element("parameters").elements();
        for (final Element paramElement : paramElements) {
            final Parameter parameter = new Parameter();
            parameter.setName(paramElement.attributeValue("name"));
            parameter.setValue(paramElement.attributeValue("value"));
            this.parameters.add(parameter);

            this.log("Added module parameter" + parameter, Project.MSG_INFO);
        }
        // Just for debugging STOP

        final Element explorertypesElement = moduleElement.element("explorertypes");
        this.importExplorertypes(explorertypesElement);
        final Element resourcetypesElement = moduleElement.element("resourcetypes");
        this.importResourcetypes(resourcetypesElement);
        final List<Element> fileElements = rootElement.element("files").elements();
        this.processFileEntries(fileElements);

    }

    /**
     * Reads the manifest.xml of the module to import.
     * <p>
     * 
     * @return the parsed manifest.xml of the module to import.
     */
    private Document readManifestXML() {
        this.log("Reading module manifest.xml.", Project.MSG_INFO);

        final SAXReader saxReader = new SAXReader();
        final File importModuleFolder = new File(this.m_importmodulefolder);
        this.log("Searching module folder " + importModuleFolder.getAbsolutePath(), Project.MSG_DEBUG);

        if (!importModuleFolder.exists()) {
            throw new BuildException("importmodulefolder \"" + this.m_importmodulefolder + "\" does not exist.");
        }
        this.log("Searching module manifest.xml...", Project.MSG_DEBUG);
        final File manifestXmlFile = new File(importModuleFolder, "manifest.xml");
        if (!manifestXmlFile.exists()) {
            throw new BuildException("No manifest.xml file in importmodulefolder \"" + this.m_importmodulefolder + "\" found.");
        }
        Document result;
        try {
            this.log("Parsing module manifest.xml...", Project.MSG_DEBUG);
            result = saxReader.read(manifestXmlFile);
            return result;
        } catch (final DocumentException e) {
            throw new BuildException("Manifest.xml \"" + manifestXmlFile.getAbsolutePath() + "\" cannot be parsed as XML.", e);
        }
    }

    public void setExplorertypes(final String explorerTypes) {
        this.explorertypes = explorerTypes;
    }

    public void setImportcontentfiles(final String importContentFiles) {
        this.m_importContentFiles = Boolean.parseBoolean(importContentFiles);
    }

    public void setImportmodulefolder(final String importmodulefolder) {
        this.m_importmodulefolder = importmodulefolder;
        if (!this.m_importmodulefolder.endsWith("/")) {
            this.m_importmodulefolder += "/";
        }
    }

    public void setImportpropertyfiles(final String importpropertyfiles) {
        this.m_importPropertyFiles = Boolean.parseBoolean(importpropertyfiles);
    }

    /**
     * Sets the build tree location of the module.
     * <p>
     * 
     * @param buildTreeFolder
     *            the build tree location of the module.
     */
    public void setModuleBuildtreefolder(final String buildTreeFolder) {
        this.m_moduleBuildTreeFolder = buildTreeFolder;
        if (!this.m_moduleBuildTreeFolder.endsWith("/")) {
            this.m_moduleBuildTreeFolder += "/";
        }
    }

    /**
     * Allows ANT to set the path to the module info xml file to generate from the task attribute.
     * <p>
     * 
     * @param moduleinfo
     *            the path to the module info xml file to generate.
     */
    public void setModuleinfo(String moduleinfo) {
        this.m_moduleinfo = moduleinfo;
    }

    public void setResourcetypes(final String resourceTypes) {
        this.m_resourcetypes = resourceTypes;
    }

    private void writePropertiesToMetaFile(final List<Element> propertiesElements, final PrintWriter metaFileStream) {

        this.log("    Writing properties.", Project.MSG_VERBOSE);
        String propertyName;
        String propertyValue;
        String propertyType;
        boolean propertyIndividual;
        String line;
        line = "# Common properties following: ";
        metaFileStream.println(line);

        StringBuilder lineBuilder;

        for (final Element property : propertiesElements) {
            propertyName = property.elementText("name");
            propertyValue = property.elementText("value");
            propertyType = property.attributeValue("type");
            if (null == propertyType) {
                propertyType = "individual";
            }
            propertyIndividual = ("shared".equals(propertyType)) ? false : true;
            this.log("      Writing " + propertyType + " property " + propertyName + " with value " + propertyValue, Project.MSG_DEBUG);
            lineBuilder = new StringBuilder(propertyName);
            lineBuilder.append('.');
            if (propertyIndividual) {
                lineBuilder.append('i');
            } else {
                lineBuilder.append('s');
            }
            lineBuilder.append(" = ");
            lineBuilder.append(propertyValue);
            line = lineBuilder.toString();
            metaFileStream.println(line);
        }

    }

    /**
     * Writes the given XML element to the given file.
     * <p>
     * 
     * @param xml
     *            the XML to write.
     * 
     * @param file
     *            the target file.
     */
    private void writeXmlSnippet(final Element xml, final File file) {
        Writer out = null;
        try {
            try {
                out = new OutputStreamWriter(new BufferedOutputStream(new FileOutputStream(file)));
                out.write(xml.asXML());

            } finally {
                if (out != null) {
                    out.flush();
                    out.close();
                }
            }
        } catch (final IOException ioex) {
            throw new BuildException("Could not write XML to file " + file.getAbsolutePath(), ioex);
        }
    }
}
